1 /**
2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 Code copied from dextool
11 */
12 module code_checker.database.schema;
13 
14 import logger = std.experimental.logger;
15 import std.array : array, empty;
16 import std.datetime : SysTime, dur, Clock;
17 import std.exception : collectException;
18 import std.format : format;
19 
20 import d2sqlite3 : SqlDatabase = Database;
21 import miniorm : Miniorm, TableName, buildSchema, ColumnParam, TableForeignKey, TableConstraint,
22     TablePrimaryKey, KeyRef, KeyParam, ColumnName, delete_, insert, select, spinSql;
23 import my.path : AbsolutePath;
24 
25 /** Initialize or open an existing database.
26  *
27  * Params:
28  *  p = path where to initialize a new database or open an existing
29  *
30  * Returns: an open sqlite3 database object.
31  */
32 Miniorm initializeDB(AbsolutePath p) @trusted
33 in {
34     assert(p.length != 0);
35 }
36 do {
37     import std.file : exists;
38     import my.file : followSymlink;
39     import my.optional;
40     import my.path : Path;
41     import d2sqlite3 : SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE;
42 
43     static void setPragmas(ref SqlDatabase db) {
44         // dfmt off
45         auto pragmas = [
46             // required for foreign keys with cascade to work
47             "PRAGMA foreign_keys=ON;",
48         ];
49         // dfmt on
50 
51         foreach (p; pragmas) {
52             db.run(p);
53         }
54     }
55 
56     const isOldDb = exists(followSymlink(Path(p)).orElse(Path(p)).toString);
57     SqlDatabase sqliteDb;
58     scope (success)
59         setPragmas(sqliteDb);
60 
61     logger.trace("Opening database ", p);
62     try {
63         sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE);
64     } catch (Exception e) {
65         logger.trace(e.msg);
66         logger.trace("Initializing a new sqlite3 database");
67         sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
68     }
69 
70     auto db = Miniorm(sqliteDb);
71 
72     auto tbl = makeUpgradeTable;
73     const longTimeout = 10.dur!"minutes";
74     try {
75         if (isOldDb
76                 && spinSql!(() => getSchemaVersion(db))(10.dur!"seconds") >= tbl
77                 .latestSchemaVersion)
78             return db;
79     } catch (Exception e) {
80         logger.info("The database is probably locked. Will keep trying to open for ", longTimeout);
81     }
82     if (isOldDb && spinSql!(() => getSchemaVersion(db))(longTimeout) >= tbl.latestSchemaVersion)
83         return db;
84 
85     // TODO: remove all key off in upgrade schemas.
86     const giveUpAfter = Clock.currTime + longTimeout;
87     bool failed = true;
88     while (failed && Clock.currTime < giveUpAfter) {
89         try {
90             auto trans = db.transaction;
91             db.run("PRAGMA foreign_keys=OFF;");
92             upgrade(db, tbl);
93             trans.commit;
94             failed = false;
95         } catch (Exception e) {
96             logger.trace(e.msg);
97         }
98     }
99 
100     if (failed) {
101         logger.error("Unable to upgrade the database to the latest schema");
102         throw new Exception(null);
103     }
104 
105     return db;
106 }
107 
108 struct UpgradeTable {
109     alias UpgradeFunc = void function(ref Miniorm db);
110     UpgradeFunc[long] tbl;
111     alias tbl this;
112 
113     immutable long latestSchemaVersion;
114 }
115 
116 /** Inspects a module for functions starting with upgradeV to create a table of
117  * functions that can be used to upgrade a database.
118  */
119 UpgradeTable makeUpgradeTable() {
120     import std.algorithm : sort, startsWith;
121     import std.conv : to;
122     import std.typecons : Tuple;
123 
124     immutable prefix = "upgradeV";
125 
126     alias Module = code_checker.database.schema;
127 
128     // the second parameter is the database version to upgrade FROM.
129     alias UpgradeFx = Tuple!(UpgradeTable.UpgradeFunc, long);
130 
131     UpgradeFx[] upgradeFx;
132     long last_from;
133 
134     static foreach (member; __traits(allMembers, Module)) {
135         static if (member.startsWith(prefix))
136             upgradeFx ~= UpgradeFx(&__traits(getMember, Module, member),
137                     member[prefix.length .. $].to!long);
138     }
139 
140     typeof(UpgradeTable.tbl) tbl;
141     foreach (fn; upgradeFx.sort!((a, b) => a[1] < b[1])) {
142         last_from = fn[1];
143         tbl[last_from] = fn[0];
144     }
145 
146     return UpgradeTable(tbl, last_from + 1);
147 }
148 
149 void updateSchemaVersion(ref Miniorm db, long ver) nothrow {
150     try {
151         db.run(delete_!VersionTbl);
152         db.run(insert!VersionTbl.insert, VersionTbl(ver));
153     } catch (Exception e) {
154         logger.error(e.msg).collectException;
155     }
156 }
157 
158 long getSchemaVersion(ref Miniorm db) {
159     auto v = db.run(select!VersionTbl);
160     return v.empty ? 0 : v.front.version_;
161 }
162 
163 void upgrade(ref Miniorm db, UpgradeTable tbl) {
164     import d2sqlite3;
165 
166     immutable maxIndex = 30;
167 
168     alias upgradeFunc = void function(ref Miniorm db);
169 
170     bool hasUpdated;
171 
172     bool running = true;
173     while (running) {
174         const version_ = () {
175             // first time the version table do not exist thus fail.
176             try {
177                 return getSchemaVersion(db);
178             } catch (Exception e) {
179             }
180             return 0;
181         }();
182 
183         if (version_ >= tbl.latestSchemaVersion) {
184             running = false;
185             break;
186         }
187 
188         logger.infof("Upgrading database from %s", version_).collectException;
189 
190         if (!hasUpdated)
191             try {
192                 // only do this once and always before any changes to the database.
193                 foreach (i; 0 .. maxIndex) {
194                     db.run(format!"DROP INDEX IF EXISTS i%s"(i));
195                 }
196             } catch (Exception e) {
197                 logger.warning(e.msg).collectException;
198                 logger.warning("Unable to drop database indexes").collectException;
199             }
200 
201         if (auto f = version_ in tbl) {
202             try {
203                 hasUpdated = true;
204 
205                 (*f)(db);
206                 if (version_ != 0)
207                     updateSchemaVersion(db, version_ + 1);
208             } catch (Exception e) {
209                 logger.trace(e).collectException;
210                 logger.error(e.msg).collectException;
211                 logger.warningf("Unable to upgrade a database of version %s",
212                         version_).collectException;
213                 logger.warning("This might impact the functionality. It is unwise to continue")
214                     .collectException;
215                 throw e;
216             }
217         } else {
218             logger.info("Upgrade successful").collectException;
219             running = false;
220         }
221     }
222 }
223 
224 immutable schemaVersionTable = "schema_version";
225 @TableName(schemaVersionTable)
226 struct VersionTbl {
227     @ColumnName("version")
228     long version_;
229 }
230 
231 immutable filesTable = "files";
232 @TableName(filesTable)
233 @TableConstraint("unique_ UNIQUE (path)")
234 struct FilesTbl {
235     long id;
236     string path;
237     long checksum;
238 
239     /// True if the file is a root.
240     bool root;
241 
242     @ColumnName("time_stamp")
243     SysTime timeStamp;
244 }
245 
246 immutable depFileTable = "dependency_file";
247 /** Files that roots are dependent on. They do not need to contain mutants.
248  */
249 @TableName(depFileTable)
250 @TableConstraint("unique_ UNIQUE (file)")
251 struct DependencyFileTable {
252     long id;
253     string file;
254     long checksum;
255 
256     @ColumnName("time_stamp")
257     SysTime timeStamp;
258 }
259 
260 immutable depRootTable = "rel_dependency_root";
261 @TableName(depRootTable)
262 @TableForeignKey("dep_id", KeyRef("dependency_file(id)"), KeyParam("ON DELETE CASCADE"))
263 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
264 @TableConstraint("unique_ UNIQUE (dep_id, file_id)")
265 struct DependencyRootTable {
266     @ColumnName("dep_id")
267     long depFileId;
268 
269     @ColumnName("file_id")
270     long fileId;
271 }
272 
273 immutable compileDbTrack = "compile_db_track";
274 @TableName(compileDbTrack)
275 @TableConstraint("unique_ UNIQUE (path)")
276 struct CompileDbTrackTable {
277     long id;
278 
279     string path;
280 
281     @ColumnName("time_stamp")
282     SysTime timeStamp;
283 
284     long checksum;
285 }
286 
287 /** If the database start it version 0, not initialized, then initialize to the
288  * latest schema version.
289  */
290 void upgradeV0(ref Miniorm db) {
291     auto tbl = makeUpgradeTable;
292 
293     db.run(buildSchema!(VersionTbl, FilesTbl, DependencyFileTable,
294             DependencyRootTable, CompileDbTrackTable));
295     updateSchemaVersion(db, tbl.latestSchemaVersion);
296 }
297 
298 void upgradeV1(ref Miniorm db) {
299     import miniorm : toSqliteDateTime;
300 
301     immutable newTbl = "new_" ~ filesTable;
302     db.run(buildSchema!FilesTbl("new_"));
303     auto stmt = db.prepare(format(
304             "INSERT INTO %s (id,path,checksum,root,time_stamp) SELECT id,path,checksum,root,:ts FROM %s",
305             newTbl, filesTable));
306     stmt.get.bind(":ts", Clock.currTime.toSqliteDateTime);
307     stmt.get.execute;
308 
309     replaceTbl(db, newTbl, filesTable);
310     db.run("DROP TABLE " ~ depFileTable);
311     db.run("DELETE FROM " ~ depRootTable);
312     db.run(buildSchema!(DependencyFileTable, FilesTbl));
313 }
314 
315 void upgradeV2(ref Miniorm db) {
316     @TableName(compileDbTrack)
317     @TableConstraint("unique_ UNIQUE (path)")
318     struct CompileDbTrackTable {
319         long id;
320 
321         string path;
322 
323         long size;
324 
325         @ColumnName("time_stamp")
326         SysTime timeStamp;
327 
328     }
329 
330     db.run(buildSchema!CompileDbTrackTable);
331 }
332 
333 void upgradeV3(ref Miniorm db) {
334     db.run("DROP TABLE " ~ compileDbTrack);
335     db.run(buildSchema!CompileDbTrackTable);
336 }
337 
338 void replaceTbl(ref Miniorm db, string src, string dst) {
339     db.run("DROP TABLE " ~ dst);
340     db.run(format("ALTER TABLE %s RENAME TO %s", src, dst));
341 }